7.2 执行机制

接口使用一个名为itab的结构存储运行期所需的相关类型信息。

type iface struct{ 
   tab *itab          // 类型信息 
   data unsafe.Pointer      // 实际对象指针 
} 
  
type itab struct{ 
   inter *interfacetype // 接口类型 
    _type *_type   // 实际对象类型 
   fun   [1]uintptr     // 实际对象方法地址 
}

利用调试器,我们可查看这些结构存储的具体内容。

type Ner interface{ 
   a() 
   b(int) 
   c(string)string
} 
  
type N int
func(N) a() {} 
func(*N)b(int) {} 
func(*N)c(string)string{return"" } 
  
func main() { 
   var n N
   var t Ner= &n
  
   t.a() 
}

输出:

$go build-gcflags"-N-l" 
  
$gdb test
  
... 
  
(gdb)info locals          # 设置断点,运行,查看局部变量信息 
&n=0xc82000a130
t= { 
 tab=0x12f028, 
 data=0xc82000a130
} 
  
(gdb)p*t.tab.inter.typ._string     # 接口类型名称 
$17=0x737f0"main.Ner" 
  
(gdb)p*t.tab._type._string    # 实际对象类型 
$20=0x707a0"*main.N" 
  
(gdb)p t.tab.inter.mhdr    # 接口类型方法集 
$27= { 
 array=0x60158<type.*+72888>, 
 len=3, 
 cap=3
} 
  
(gdb)p*t.tab.inter.mhdr.array[0].name   # 接口方法名称 
$30=0x70a48"a" 
  
(gdb)p*t.tab.inter.mhdr.array[1].name
$31=0x70b08"b" 
  
(gdb)p*t.tab.inter.mhdr.array[2].name
$32=0x70ba0"c" 
  
(gdb)info symbol t.tab.fun[0]        # 实际对象方法地址 
main.(*N).a in section.text
  
(gdb)info symbol t.tab.fun[1] 
main.(*N).b in section.text
  
(gdb)info symbol t.tab.fun[2] 
main.(*N).c in section.text

很显然,相关类型信息里保存了接口和实际对象的元数据。同时,itab还用fun数组(不定长结构)保存了实际方法地址,从而实现在运行期对目标方法的动态调用。

除此之外,接口还有一个重要特征:将对象赋值给接口变量时,会复制该对象。

type data struct{ 
   x int
} 
  
func main() { 
   d:=data{100} 
   var t interface{} =d
  
   println(t.(data).x) 
}

输出:

$go build-gcflags"-N-l" 
  
$gdb test
  
(gdb)info locals          # 输出局部变量 
d= { 
 x=100
} 
t= { 
  _type=0x5ec00<type.*+67296>, 
 data=0xc820035f20           # 接口变量存储的对象地址 
} 
  
(gdb)p/x&d              # 局部变量地址。显然和接口存储的不是同一对象 
$1=0xc820035f10

我们甚至无法修改接口存储的复制品,因为它也是unaddressable的。

func main() { 
   d:=data{100} 
   var t interface{} =d
  
   p:= &t.(data)           // 错误:cannot take the address of t.(data) 
   t.(data).x=200         // 错误:cannot assign to t.(data).x
}

即便将其复制出来,用本地变量修改后,依然无法对iface.data赋值。解决方法就是将对象指针赋值给接口,那么接口内存储的就是指针的复制品。

func main() { 
   d:=data{100} 
   var t interface{} = &d
  
   t.(*data).x=200
   println(t.(*data).x) 
}
 

输出:

$go build-gcflags"-N-l" && ./test
  
200
  
$gdb test
  
(gdb)info locals              # 显示局部变量 
d= { 
 x=100
} 
t= { 
  _type=0x50480<type.*+8096>, 
 data=0xc820035f10
} 
  
(gdb)p/x&d                  # 显然和接口内data存储的地址一致 
$1=0xc820035f10

只有当接口变量内部的两个指针(itab,data)都为nil时,接口才等于nil。

func main() { 
   var a interface{} =nil
   var b interface{} = (*int)(nil) 
  
   println(a==nil,b==nil) 
}

输出:

true false
  
(gdb)info locals
  
b= { 
  _type=0x500c0<type.*+7616>,       # 显然b包含了类型信息 
 data=0x0
} 
a= { 
  _type=0x0, 
 data=0x0
}

由此造成的错误并不罕见,尤其是在函数返回error时。

type TestError struct{} 
  
func(*TestError)Error()string{ 
   return"error" 
} 
  
func test(x int) (int,error) { 
   var err*TestError
  
   if x<0{ 
       err=new(TestError) 
       x=0
    }else{ 
       x+=100
    } 
  
   return x,err               // 注意: 这个err是有类型的 
} 
  
func main() { 
   x,err:=test(100) 
   if err!=nil{ 
       log.Fatalln("err!=nil")        // 此处被执行 
    } 
  
   println(x) 
}

输出:

2020/01/01 19:48:27 err!=nil
exit status 1
  
  
(gdb)info locals              # 很显然x没问题,但err并不等于nil
x=200
err= { 
 tab=0x2161e8,                #tab!=nil
 data=0x0
}

正确做法是明确返回nil。

func test(x int) (int,error) { 
   if x<0{ 
       return 0,new(TestError) 
    } 
  
   return x+100,nil
}